2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIAccountController.h"
18 #import "AIContactController.h"
19 #import "AIContentController.h"
20 #import "AIInterfaceController.h"
21 #import "AIStatusController.h"
22 #import "AIPreferenceController.h"
23 #import "CBGaimAccount.h"
24 #import "SLGaimCocoaAdapter.h"
25 #import <AIUtilities/AIAttributedStringAdditions.h>
26 #import <AIUtilities/AIDictionaryAdditions.h>
27 #import <AIUtilities/AIMenuAdditions.h>
28 #import <AIUtilities/AIMutableOwnerArray.h>
29 #import <AIUtilities/AIStringAdditions.h>
30 #import <AIUtilities/CBApplicationAdditions.h>
31 #import <AIUtilities/CBObjectAdditions.h>
32 #import <AIUtilities/ESImageAdditions.h>
33 #import <AIUtilities/ESSystemNetworkDefaults.h>
34 #import <Adium/AIAccount.h>
35 #import <Adium/AIChat.h>
36 #import <Adium/AIContentMessage.h>
37 #import <Adium/AIHTMLDecoder.h>
38 #import <Adium/AIListContact.h>
39 #import <Adium/AIListGroup.h>
40 #import <Adium/AIMetaContact.h>
41 #import <Adium/AIService.h>
42 #import <Adium/AIServiceIcons.h>
43 #import <Adium/AIStatus.h>
44 #import <Adium/ESFileTransfer.h>
45 #import <Adium/AIWindowController.h>
47 #define NO_GROUP @"__NoGroup__"
49 #define AUTO_RECONNECT_DELAY 2.0 //Delay in seconds
50 #define RECONNECTION_ATTEMPTS 4
52 #define PREF_GROUP_ALIASES @"Aliases" //Preference group to store aliases in
54 @interface CBGaimAccount (PRIVATE)
58 - (void)setBuddyImageFromFilename:(char *)imageFilename;
59 - (NSString *)_userIconCachePath;
60 - (void)_setInstantMessagesWithContact:(AIListContact *)contact enabled:(BOOL)enable;
62 - (NSString *)_mapIncomingGroupName:(NSString *)name;
63 - (NSString *)_mapOutgoingGroupName:(NSString *)name;
65 - (NSString *)displayServiceIDForUID:(NSString *)aUID;
67 //- (void)_updateAllEventsForBuddy:(GaimBuddy*)buddy;
68 - (void)setTypingFlagOfChat:(AIChat *)inChat to:(NSNumber *)typingState;
69 - (void)_updateAway:(AIListContact *)theContact toAway:(BOOL)newAway;
71 - (AIChat*)_openChatWithContact:(AIListContact *)contact andConversation:(GaimConversation*)conv;
73 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(GaimMessageFlags)flags date:(NSDate *)date;
74 - (void)_sentMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat toDestinationListContact:(AIListContact *)destinationContact flags:(GaimMessageFlags)flags date:(NSDate *)date;
75 - (NSString *)_processGaimImagesInString:(NSString *)inString;
76 - (NSString *)_handleFileSendsWithinMessage:(NSString *)inString toContact:(AIListContact *)listContact contentMessage:(AIContentMessage *)contentMessage;
77 - (NSString *)_messageImageCachePathForID:(int)imageID;
79 - (ESFileTransfer *)createFileTransferObjectForXfer:(GaimXfer *)xfer;
81 - (void)displayError:(NSString *)errorDesc;
82 - (NSNumber *)shouldCheckMail;
84 - (void)updateStatusForKey:(NSString *)key immediately:(BOOL)immediately;
86 - (void)configureGaimAccountNotifyingTarget:(id)target selector:(SEL)selector;
90 @implementation CBGaimAccount
92 static BOOL didInitSSL = NO;
94 static SLGaimCocoaAdapter *gaimThread = nil;
96 // The GaimAccount currently associated with this Adium account
97 - (GaimAccount*)gaimAccount
99 //Create a gaim account if one does not already exist
101 [self createNewGaimAccount];
102 GaimDebug(@"%x: created GaimAccount 0x%x with UID %@, protocolPlugin %s", [NSRunLoop currentRunLoop],account, [self UID], [self protocolPlugin]);
108 - (SLGaimCocoaAdapter *)gaimThread
116 didInitSSL = gaim_init_ssl_openssl_plugin();
120 // Subclasses must override this
121 - (const char*)protocolPlugin { return NULL; }
123 // Contacts ------------------------------------------------------------------------------------------------
124 #pragma mark Contacts
125 - (oneway void)newContact:(AIListContact *)theContact withName:(NSString *)inName
130 - (oneway void)updateContact:(AIListContact *)theContact toGroupName:(NSString *)groupName contactName:(NSString *)contactName
132 //A quick sign on/sign off can leave these messages in the threaded messaging queue... we most definitely don't want
133 //to put the contact back into a remote group after signing off, as a ghost will appear. Spooky!
134 if([self online] || [self integerStatusObjectForKey:@"Connecting"]){
135 //When a new contact is created, if we aren't already silent and delayed, set it a second to cover our initial
137 if(!silentAndDelayed){
138 [self silenceAllContactUpdatesForInterval:2.0];
139 [[adium contactController] delayListObjectNotificationsUntilInactivity];
142 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
143 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
144 if(![contactName isEqualToString:[theContact formattedUID]] && ![contactName isEqualToString:[theContact UID]]){
145 [theContact setStatusObject:contactName
146 forKey:@"FormattedUID"
150 if(groupName && [groupName isEqualToString:@GAIM_ORPHANS_GROUP_NAME]){
151 [theContact setRemoteGroupName:AILocalizedString(@"Orphans","Name for the orphans group")];
152 }else if(groupName && [groupName length] != 0){
153 [theContact setRemoteGroupName:[self _mapIncomingGroupName:groupName]];
155 [theContact setRemoteGroupName:[self _mapIncomingGroupName:nil]];
158 [self gotGroupForContact:theContact];
160 GaimDebug(@"Got %@ for %@ while not online",groupName,theContact);
165 * @brief Change the UID of a contact
167 * If we're just passed a formatted version of the current UID, don't change the UID but instead use the information
168 * as the FormattedUID. For example, we get sent this when an AIM contact's name formatting changes; we always want
169 * to use a lowercase and space-free version for the UID, however.
171 - (void)renameContact:(AIListContact *)theContact toUID:(NSString *)newUID
173 //If the name we were passed differs from the current formatted UID of the contact, it's itself a formatted UID
174 //This is important since we may get an alias ("Evan Schoenberg") from the server but also want the formatted name
175 NSString *filteredUID = [[self service] filterUID:newUID removeIgnoredCharacters:YES];
177 if ([filteredUID isEqualToString:[theContact UID]]) {
178 [theContact setStatusObject:newUID
179 forKey:@"FormattedUID"
182 [theContact setUID:newUID];
186 - (void)applyAlias:(NSString *)gaimAlias toContact:(AIListContact *)theContact
189 BOOL displayNameChanges = NO;
191 //Store this alias as the serverside display name so long as it isn't identical when unformatted to the UID
192 if(![[gaimAlias compactedString] isEqualToString:[[theContact UID] compactedString]]){
194 //This is the server display name. Set it as such.
195 if(![gaimAlias isEqualToString:[theContact statusObjectForKey:@"Server Display Name"]]){
196 //Set the server display name status object as the full display name
197 [theContact setStatusObject:gaimAlias
198 forKey:@"Server Display Name"
204 //Use it either as the status message or the display name.
205 if ([self useDisplayNameAsStatusMessage]){
206 if (![[theContact stringFromAttributedStringStatusObjectForKey:@"ContactListStatusMessage"] isEqualToString:gaimAlias]){
207 [theContact setStatusObject:[[[NSAttributedString alloc] initWithString:gaimAlias] autorelease]
208 forKey:@"ContactListStatusMessage"
215 AIMutableOwnerArray *displayNameArray = [theContact displayArrayForKey:@"Display Name"];
216 NSString *oldDisplayName = [displayNameArray objectValue];
218 //If the mutableOwnerArray's current value isn't identical to this alias, we should set it
219 if(![[displayNameArray objectWithOwner:self] isEqualToString:gaimAlias]){
220 [displayNameArray setObject:gaimAlias
222 priorityLevel:Low_Priority];
224 //If this causes the object value to change, we need to request a manual update of the display name
225 if(oldDisplayName != [displayNameArray objectValue]){
226 displayNameChanges = YES;
232 if(![gaimAlias isEqualToString:[theContact formattedUID]] && ![gaimAlias isEqualToString:[theContact UID]]){
233 [theContact setStatusObject:gaimAlias
234 forKey:@"FormattedUID"
243 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
246 if (displayNameChanges){
247 //Notify of display name changes
248 [[adium contactController] listObjectAttributesChanged:theContact
249 modifiedKeys:[NSSet setWithObject:@"Display Name"]];
251 //XXX - There must be a cleaner way to do this alias stuff! This works for now
252 //Request an alias change
253 [[adium notificationCenter] postNotificationName:Contact_ApplyDisplayName
255 userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES]
260 - (void)updateContact:(AIListContact *)theContact toAlias:(NSString *)gaimAlias
262 [self applyAlias:gaimAlias toContact:theContact];
265 - (BOOL)useDisplayNameAsStatusMessage
270 - (oneway void)updateContact:(AIListContact *)theContact forEvent:(NSNumber *)event
276 - (oneway void)updateSignon:(AIListContact *)theContact withData:(void *)data
278 NSNumber *contactOnlineStatus = [theContact statusObjectForKey:@"Online"];
280 if(!contactOnlineStatus || ([contactOnlineStatus boolValue] != YES)){
281 [theContact setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Online" notify:NO];
283 if(!silentAndDelayed){
284 [theContact setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Signed On" notify:NO];
285 [theContact setStatusObject:nil forKey:@"Signed Off" notify:NO];
286 [theContact setStatusObject:nil forKey:@"Signed On" afterDelay:15];
290 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
295 - (oneway void)updateSignoff:(AIListContact *)theContact withData:(void *)data
297 NSNumber *contactOnlineStatus = [theContact statusObjectForKey:@"Online"];
298 if(contactOnlineStatus && ([contactOnlineStatus boolValue] != NO)){
299 if(!silentAndDelayed){
300 [theContact setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Signed Off" notify:NO];
301 [theContact setStatusObject:nil forKey:@"Signed On" notify:NO];
302 [theContact setStatusObject:nil forKey:@"Signed Off" afterDelay:15];
305 //Will also apply any changes applied above, so no need to call notifyOfChangedStatusSilently
306 [self removeStatusObjectsFromContact:theContact silently:silentAndDelayed];
311 - (oneway void)updateSignonTime:(AIListContact *)theContact withData:(NSDate *)signonDate
314 //Set the signon time
315 [theContact setStatusObject:signonDate
316 forKey:@"Signon Date"
320 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
325 * @brief Status name to use for a Gaim buddy
327 * Called by SLGaimCocoaAdapter on the gaim thread
329 - (NSString *)statusNameForGaimBuddy:(GaimBuddy *)b
335 * @brief Status message for a contact
337 * Called by SLGaimCocoaAdapter on the gaim thread
339 - (NSAttributedString *)statusMessageForGaimBuddy:(GaimBuddy *)b
345 * @brief Update the status message and away state of the contact
347 * Called by SLGaimCocoaAdapter on the main thread
349 - (void)updateStatusForContact:(AIListContact *)theContact toStatusType:(NSNumber *)statusTypeNumber statusName:(NSString *)statusName statusMessage:(NSAttributedString *)statusMessage
351 [theContact setStatusWithName:statusName
352 statusType:[statusTypeNumber intValue]
354 [theContact setStatusMessage:statusMessage
358 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
362 * @brief Update the status state of the contact
364 * Called by SLGaimCocoaAdapter on the main thread
366 - (void)updateStatusForContact:(AIListContact *)theContact toStatusType:(NSNumber *)statusTypeNumber statusName:(NSString *)statusName
368 [theContact setStatusWithName:statusName
369 statusType:[statusTypeNumber intValue]
373 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
377 * @brief Update the status message of the contact
379 * Called by SLGaimCocoaAdapter on the main thread
381 - (void)updateStatusForContact:(AIListContact *)theContact toStatusMessage:(NSAttributedString *)statusMessage
383 [theContact setStatusMessage:statusMessage
387 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
391 - (void)updateWentIdle:(AIListContact *)theContact withData:(NSDate *)idleSinceDate
394 [theContact setStatusObject:idleSinceDate
398 //No idleSinceDate means we are Idle but don't know how long, so set to -1
399 [theContact setStatusObject:[NSNumber numberWithInt:-1]
404 //@"Idle", for a contact with an IdleSince date, will be changing every minute. @"IsIdle" provides observers a way
405 //to perform an action when the contact becomes/comes back from idle, regardless of whether an IdleSince is available,
406 //without having to do that action every minute for other contacts.
407 [theContact setStatusObject:[NSNumber numberWithBool:YES]
412 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
414 - (void)updateIdleReturn:(AIListContact *)theContact withData:(void *)data
416 [theContact setStatusObject:nil
419 [theContact setStatusObject:nil
423 [theContact setStatusObject:nil
428 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
431 //Evil level (warning level)
432 - (oneway void)updateEvil:(AIListContact *)theContact withData:(NSNumber *)evilNumber
434 //Set the warning level or clear it if it's now 0.
435 int evil = [evilNumber intValue];
436 NSNumber *currentWarningLevel = [theContact statusObjectForKey:@"Warning"];
439 if (!currentWarningLevel || ([currentWarningLevel intValue] != evil)) {
440 [theContact setStatusObject:evilNumber
444 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
447 if (currentWarningLevel) {
448 [theContact setStatusObject:nil
452 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
459 - (oneway void)updateIcon:(AIListContact *)theContact withData:(NSData *)userIconData
462 //Observers get a single shot at utilizing the user icon data in its raw form
463 [theContact setStatusObject:userIconData forKey:@"UserIconData" notify:NO];
465 //Set the User Icon as an NSImage
466 NSImage *userIcon = [[NSImage alloc] initWithData:userIconData];
467 [theContact setStatusObject:userIcon forKey:KEY_USER_ICON notify:NO];
471 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
473 //Clear the UserIconData
474 [theContact setStatusObject:nil forKey:@"UserIconData" notify:NO];
478 - (oneway void)updateUserInfo:(AIListContact *)theContact withData:(NSString *)userInfoString
480 NSString *oldUserInfoString = [theContact statusObjectForKey:@"TextProfileString"];
482 if (userInfoString && [userInfoString length]) {
483 if (![userInfoString isEqualToString:oldUserInfoString]) {
485 [theContact setStatusObject:userInfoString
486 forKey:@"TextProfileString"
488 [theContact setStatusObject:[AIHTMLDecoder decodeHTML:userInfoString]
489 forKey:@"TextProfile"
492 } else if (oldUserInfoString) {
493 [theContact setStatusObject:nil forKey:@"TextProfileString" notify:NO];
494 [theContact setStatusObject:nil forKey:@"TextProfile" notify:NO];
498 [theContact notifyOfChangedStatusSilently:silentAndDelayed];
502 * @brief Gaim removed a contact from the local blist
504 * This can happen in many situations:
505 * - For every contact on an account when the account signs off
506 * - For a contact as it is deleted by the user
507 * - For a contact as it is deleted by Gaim (e.g. when Sametime refuses an addition because it is known to be invalid)
508 * - In the middle of the move process as a contact moves from one group to another
510 * We need not take any action; we'll be notified of changes by Gaim as necessary.
512 - (oneway void)removeContact:(AIListContact *)theContact
517 //To allow root level buddies on protocols which don't support them, we map any buddies in a group
518 //named after this account's UID to the root group. These functions handle the mapping. Group names should
519 //be filtered through incoming before being sent to Adium - and group names from Adium should be filtered through
520 //outgoing before being used.
521 - (NSString *)_mapIncomingGroupName:(NSString *)name
523 if(!name || ([[name compactedString] caseInsensitiveCompare:[self UID]] == 0)){
524 return(ADIUM_ROOT_GROUP_NAME);
529 - (NSString *)_mapOutgoingGroupName:(NSString *)name
531 if([[name compactedString] caseInsensitiveCompare:ADIUM_ROOT_GROUP_NAME] == 0){
538 //Update the status of a contact (Request their profile)
539 - (void)delayedUpdateContactStatus:(AIListContact *)inContact
542 AILog(@"%@: Update %@ : %i %i",self,inContact,[inContact online],[inContact isStranger]);
543 // if ([inContact online] || [inContact isStranger]){
544 [gaimThread getInfoFor:[inContact UID] onAccount:self];
548 - (oneway void)requestAddContactWithUID:(NSString *)contactUID
550 [[adium contactController] requestAddContactWithUID:contactUID
551 service:[self _serviceForUID:contactUID]];
554 - (AIService *)_serviceForUID:(NSString *)contactUID
556 return([self service]);
559 - (void)gotGroupForContact:(AIListContact *)listContact {};
561 /*********************/
562 /* AIAccount_Handles */
563 /*********************/
564 #pragma mark Contact List Editing
566 - (void)removeContacts:(NSArray *)objects
568 NSEnumerator *enumerator = [objects objectEnumerator];
569 AIListContact *object;
571 while(object = [enumerator nextObject]){
572 NSString *groupName = [self _mapOutgoingGroupName:[object remoteGroupName]];
574 //Have the gaim thread perform the serverside actions
575 [gaimThread removeUID:[object UID] onAccount:self fromGroup:groupName];
577 //Remove it from Adium's list
578 [object setRemoteGroupName:nil];
582 - (void)addContacts:(NSArray *)objects toGroup:(AIListGroup *)inGroup
584 NSEnumerator *enumerator = [objects objectEnumerator];
585 AIListContact *object;
586 NSString *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
588 while(object = [enumerator nextObject]){
589 [gaimThread addUID:[self _UIDForAddingObject:object] onAccount:self toGroup:groupName];
591 //Add it to Adium's list
592 [object setRemoteGroupName:[inGroup UID]]; //Use the non-mapped group name locally
596 - (NSString *)_UIDForAddingObject:(AIListContact *)object
598 return([object UID]);
601 - (void)moveListObjects:(NSArray *)objects toGroup:(AIListGroup *)group
603 NSString *groupName = [self _mapOutgoingGroupName:[group UID]];
604 NSEnumerator *enumerator;
605 AIListContact *listObject;
607 //Move the objects to it
608 enumerator = [objects objectEnumerator];
609 while(listObject = [enumerator nextObject]){
610 if([listObject isKindOfClass:[AIListGroup class]]){
611 //Since no protocol here supports nesting, a group move is really a re-name
614 // NSString *oldGroupName = [self _mapOutgoingGroupName:[listObject remoteGroupName]];
616 //Tell the gaim thread to perform the serverside operation
617 [gaimThread moveUID:[listObject UID] onAccount:self toGroup:groupName];
619 //Use the non-mapped group name locally
620 [listObject setRemoteGroupName:[group UID]];
625 - (void)renameGroup:(AIListGroup *)inGroup to:(NSString *)newName
627 NSString *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
629 //Tell the gaim thread to perform the serverside operation
630 [gaimThread renameGroup:groupName onAccount:self to:newName];
632 //We must also update the remote grouping of all our contacts in that group
633 NSEnumerator *enumerator = [[[adium contactController] allContactsInGroup:inGroup subgroups:YES onAccount:self] objectEnumerator];
634 AIListContact *contact;
636 while(contact = [enumerator nextObject]){
637 //Evan: should we use groupName or newName here?
638 [contact setRemoteGroupName:newName];
642 - (void)deleteGroup:(AIListGroup *)inGroup
644 NSString *groupName = [self _mapOutgoingGroupName:[inGroup UID]];
646 [gaimThread deleteGroup:groupName onAccount:self];
649 // Return YES if the contact list is editable
650 - (BOOL)contactListEditable
652 return([[self statusObjectForKey:@"Online"] boolValue]);
655 //Chats ------------------------------------------------------------
658 //Add a new chat - this will ultimately call -(BOOL)openChat:(AIChat *)chat below.
659 - (oneway void)addChat:(AIChat *)chat
662 [[adium contentController] openChat:chat];
665 //Open a chat for Adium
666 - (BOOL)openChat:(AIChat *)chat
668 /* The #if 0'd block below causes crashes in msn_tooltip_text() on MSN */
670 AIListContact *listContact;
672 //Obtain the contact's information if it's a stranger
673 if ((listContact = [chat listObject]) && ([listContact isStranger])){
674 [self delayedUpdateContactStatus:listContact];
678 AILog(@"gaim openChat:%@ for %@",chat,[chat uniqueChatID]);
680 //Inform gaim that we have opened this chat
681 [gaimThread openChat:chat onAccount:self];
683 //Created the chat successfully
687 - (BOOL)closeChat:(AIChat*)chat
689 [gaimThread closeChat:chat];
691 //Be sure any remaining typing flag is cleared as the chat closes
692 [self setTypingFlagOfChat:chat to:nil];
693 AILog(@"gaim closeChat:%@",[chat uniqueChatID]);
698 - (AIChat *)mainThreadChatWithContact:(AIListContact *)contact
702 //First, make sure the chat is created
703 [[adium contentController] mainPerformSelector:@selector(chatWithContact:)
707 //Now return the existing chat
708 chat = [[adium contentController] existingChatWithContact:contact];
713 - (AIChat *)mainThreadChatWithName:(NSString *)name
718 First, make sure the chat is created - we will get here from a call in which Gaim has already
719 created the GaimConversation, so there's no need for a chatCreationInfo dictionary.
722 [[adium contentController] mainPerformSelector:@selector(chatWithName:onAccount:chatCreationInfo:)
728 //Now return the existing chat
729 chat = [[adium contentController] existingChatWithName:name onAccount:self];
734 //Typing update in an IM
735 - (oneway void)typingUpdateForIMChat:(AIChat *)chat typing:(NSNumber *)typingState
737 [self setTypingFlagOfChat:chat
741 //Multiuser chat update
742 - (oneway void)convUpdateForChat:(AIChat *)chat type:(NSNumber *)type
746 - (oneway void)updateTopic:(NSString *)inTopic forChat:(AIChat *)chat
750 - (oneway void)updateTitle:(NSString *)inTitle forChat:(AIChat *)chat
752 [[chat displayArrayForKey:@"Display Name"] setObject:inTitle
756 - (oneway void)updateForChat:(AIChat *)chat type:(NSNumber *)type
758 AIChatUpdateType updateType = [type intValue];
762 if ([self displayConversationTimedOut]){
763 key = KEY_CHAT_TIMED_OUT;
767 case AIChatClosedWindow:
768 if ([self displayConversationClosed]){
769 key = KEY_CHAT_CLOSED_WINDOW;
775 [chat setStatusObject:[NSNumber numberWithBool:YES] forKey:key notify:NotifyNow];
776 [chat setStatusObject:nil forKey:key notify:NotifyNever];
781 - (oneway void)errorForChat:(AIChat *)chat type:(NSNumber *)type
783 [chat setStatusObject:type forKey:KEY_CHAT_ERROR notify:NotifyNow];
784 [chat setStatusObject:nil forKey:KEY_CHAT_ERROR notify:NotifyNever];
787 - (oneway void)receivedIMChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
789 GaimMessageFlags flags = [[messageDict objectForKey:@"GaimMessageFlags"] intValue];
790 NSAttributedString *attributedMessage;
791 AIListContact *listContact;
794 attributedMessage = [messageDict objectForKey:@"AttributedMessage"];
795 listContact = [chat listObject];
796 date = [messageDict objectForKey:@"Date"];
798 if ((flags & GAIM_MESSAGE_SEND) != 0) {
799 //Gaim is telling us that our message was sent successfully.
801 //We can now tell the other side that we're done typing
802 //[gaimThread sendTyping:AINotTyping inChat:chat];
805 //Clear the typing flag of the chat since a message was just received
806 [self setTypingFlagOfChat:chat to:nil];
808 [self _receivedMessage:attributedMessage
810 fromListContact:listContact
816 - (oneway void)receivedMultiChatMessage:(NSDictionary *)messageDict inChat:(AIChat *)chat
818 GaimMessageFlags flags = [[messageDict objectForKey:@"GaimMessageFlags"] intValue];
819 NSAttributedString *attributedMessage;
822 attributedMessage = [messageDict objectForKey:@"AttributedMessage"];
823 date = [messageDict objectForKey:@"Date"];
825 if ((flags & GAIM_MESSAGE_SEND) != 0){
826 //Gaim is telling us that our message was sent successfully.
828 //We can now tell the other side that we're done typing
829 //[gaimThread sendTyping:AINotTyping inChat:chat];
832 NSString *source = [messageDict objectForKey:@"Source"];
834 //We display the message locally when it is sent. If the protocol sends the message back to us, we should
835 //simply ignore it (MSN does this when a display name is set, for example).
836 if (![source isEqualToString:[self UID]]){
837 AIListContact *listContact;
839 //source may be (null) for system messages like topic changes
840 listContact = (source ? [self contactWithUID:source] : nil);
843 [self _receivedMessage:attributedMessage
845 fromListContact:listContact
849 //If we didn't get a listContact, this is a gaim status message... display it as such.
850 [[adium contentController] displayStatusMessage:[attributedMessage string]
859 - (void)_receivedMessage:(NSAttributedString *)attributedMessage inChat:(AIChat *)chat fromListContact:(AIListContact *)sourceContact flags:(GaimMessageFlags)flags date:(NSDate *)date
861 AIContentMessage *messageObject = [AIContentMessage messageInChat:chat
862 withSource:sourceContact
865 message:attributedMessage
866 autoreply:(flags & GAIM_MESSAGE_AUTO_RESP) != 0];
868 [[adium contentController] receiveContentObject:messageObject];
871 /*********************/
872 /* AIAccount_Content */
873 /*********************/
875 - (BOOL)sendContentObject:(AIContentObject*)object
879 if (gaim_account_is_connected(account)) {
880 if([[object type] isEqualToString:CONTENT_MESSAGE_TYPE]) {
881 AIContentMessage *contentMessage = (AIContentMessage*)object;
882 AIChat *chat = [contentMessage chat];
883 NSAttributedString *message = [contentMessage message];
884 NSString *encodedMessage;
886 //Grab the list object (which may be null if this isn't a chat with a particular listObject)
887 AIListContact *listObject = [chat listObject];
889 //Use GaimConvImFlags for now; multiuser chats will end up ignoring this
890 GaimConvImFlags flags = ([contentMessage isAutoreply] ? GAIM_CONV_IM_AUTO_RESP : 0);
892 //If this connection doesn't support new lines, send all lines before newlines as separate messages
893 if (account->gc->flags & GAIM_CONNECTION_NO_NEWLINES) {
894 NSRange endlineRange;
897 while(((endlineRange = [[message string] rangeOfString:@"\n"]).location) != NSNotFound ||
898 ((returnRange = [[message string] rangeOfString:@"\r"]).location) != NSNotFound){
900 //Use whichever endline character is found first
901 NSRange operativeRange = ((endlineRange.location < returnRange.location) ? endlineRange : returnRange);
903 if (operativeRange.location > 0){
904 NSAttributedString *thisPart;
905 NSString *thisPartString;
907 thisPart = [message attributedSubstringFromRange:NSMakeRange(0,operativeRange.location-1)];
908 thisPartString = [thisPart string];
910 encodedMessage = [self encodedAttributedString:thisPart
911 forListObject:listObject
912 contentMessage:contentMessage];
914 //Check for the AdiumFT tag indicating an embedded file transfer.
915 //Only deal with scanning deeper if it's found.
916 if ([encodedMessage rangeOfString:@"<AdiumFT "
917 options:NSCaseInsensitiveSearch].location != NSNotFound){
918 encodedMessage = [self _handleFileSendsWithinMessage:encodedMessage
920 contentMessage:contentMessage];
923 sent = [gaimThread sendEncodedMessage:encodedMessage
924 originalMessage:thisPartString
931 message = [message attributedSubstringFromRange:NSMakeRange(operativeRange.location+operativeRange.length,[[message string] length]-operativeRange.location)];
936 if ([message length]){
937 encodedMessage = [self encodedAttributedString:message
938 forListObject:listObject
939 contentMessage:contentMessage];
941 NSString *messageString;
943 //Check for the AdiumFT tag indicating an embedded file transfer.
944 //Only deal with scanning deeper if it's found.
945 if ([encodedMessage rangeOfString:@"<AdiumFT "
946 options:NSCaseInsensitiveSearch].location != NSNotFound){
947 encodedMessage = [self _handleFileSendsWithinMessage:encodedMessage
949 contentMessage:contentMessage];
952 messageString = [message string];
954 sent = [gaimThread sendEncodedMessage:encodedMessage
955 originalMessage:messageString
962 } else if ([[object type] isEqualToString:CONTENT_TYPING_TYPE]) {
963 AIContentTyping *contentTyping = (AIContentTyping*)object;
964 AIChat *chat = [contentTyping chat];
967 [gaimThread sendTyping:[contentTyping typingState] inChat:chat];
977 //Return YES if we're available for sending the specified content or will be soon (are currently connecting).
978 //If inListObject is nil, we can return YES if we will 'most likely' be able to send the content.
979 - (BOOL)availableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact
981 BOOL weAreOnline = [self online];
983 if([inType isEqualToString:CONTENT_MESSAGE_TYPE]){
984 if((weAreOnline && (inContact == nil || [inContact online])) ||
985 ([self integerStatusObjectForKey:@"Connecting"])){
988 }else if (([inType isEqualToString:FILE_TRANSFER_TYPE]) && ([self conformsToProtocol:@protocol(AIAccount_Files)])){
991 if([inContact online]){
992 return([self allowFileTransferWithListObject:inContact]);
1003 - (BOOL)allowFileTransferWithListObject:(AIListObject *)inListObject
1008 - (NSString *)_handleFileSendsWithinMessage:(NSString *)inString toContact:(AIListContact *)listContact contentMessage:(AIContentMessage *)contentMessage
1012 NSCharacterSet *tagCharStart, *tagEnd, *absoluteTagEnd;
1013 NSString *chunkString;
1014 NSMutableString *processedString;
1016 tagCharStart = [NSCharacterSet characterSetWithCharactersInString:@"<"];
1017 tagEnd = [NSCharacterSet characterSetWithCharactersInString:@" >"];
1018 absoluteTagEnd = [NSCharacterSet characterSetWithCharactersInString:@">"];
1020 scanner = [NSScanner scannerWithString:inString];
1021 [scanner setCaseSensitive:NO];
1022 [scanner setCharactersToBeSkipped:[NSCharacterSet characterSetWithCharactersInString:@""]];
1024 processedString = [[NSMutableString alloc] init];
1027 while(![scanner isAtEnd]){
1028 //Find an HTML IMG tag
1029 if([scanner scanUpToString:@"<AdiumFT" intoString:&chunkString]){
1030 [processedString appendString:chunkString];
1034 if([scanner scanCharactersFromSet:tagCharStart intoString:nil]){ //If a tag wasn't found, we don't process.
1035 // unsigned scanLocation = [scanner scanLocation]; //Remember our location (if this is an invalid tag we'll need to move back)
1037 //Get the tag itself
1038 if([scanner scanUpToCharactersFromSet:tagEnd intoString:&chunkString]){
1040 if([chunkString caseInsensitiveCompare:@"AdiumFT"] == 0){
1041 if([scanner scanUpToCharactersFromSet:absoluteTagEnd intoString:&chunkString]){
1043 //Extract the file we wish to send
1044 NSDictionary *imgArguments = [AIHTMLDecoder parseArguments:chunkString];
1045 NSString *filePath = [[imgArguments objectForKey:@"src"] stringByUnescapingFromHTML];
1048 [[adium fileTransferController] sendFile:filePath toListContact:listContact];
1052 if (![scanner isAtEnd]){
1053 [scanner setScanLocation:[scanner scanLocation]+1];
1059 /* We've removed AdiumFT tags from an arbitrarily encoded HTML string. This could leave us with no actual
1060 * text to send. If that's the case, we don't want to return something like <HTML></HTML>. Instead, we want
1061 * to return nil. We therefore decode and reencode our new string. */
1063 return ([self encodedAttributedString:[AIHTMLDecoder decodeHTML:processedString]
1064 forListObject:listContact
1065 contentMessage:contentMessage]);
1067 GaimDebug(@"Sending a file to a chat. Are you insane?");
1072 // **XXX** Not used at present. Do we want to?
1073 - (BOOL)shouldSendAutoresponsesWhileAway
1075 if (account && account->gc){
1076 return (account->gc->flags & GAIM_CONNECTION_AUTO_RESP);
1082 #pragma mark GaimConversation User Lists
1083 - (void)addUser:(NSString *)contactName toChat:(AIChat *)chat newArrival:(NSNumber *)newArrival
1085 AIListContact *listContact;
1088 (listContact = [self contactWithUID:contactName])){
1090 if (!namesAreCaseSensitive){
1091 [listContact setStatusObject:contactName forKey:@"FormattedUID" notify:NotifyNow];
1094 [chat addParticipatingListObject:listContact notify:(newArrival && [newArrival boolValue])];
1098 - (oneway void)addUsersArray:(NSArray *)usersArray toChat:(AIChat *)chat
1100 NSEnumerator *enumerator;
1101 NSString *contactName;
1103 GaimDebug(@"*** %@: addUsersArray:%@ toChat:%@",self,usersArray,chat);
1105 enumerator = [usersArray objectEnumerator];
1106 while ((contactName = [enumerator nextObject])) {
1107 [self addUser:contactName toChat:chat newArrival:nil];
1111 - (oneway void)removeUser:(NSString *)contactName fromChat:(AIChat *)chat
1113 AIListContact *contact;
1116 (contact = [self contactWithUID:contactName])){
1118 [chat removeParticipatingListObject:contact];
1120 GaimDebug(@"%@ removeUser:%@ fromChat:%@",self,contact,chat);
1124 - (oneway void)removeUsersArray:(NSArray *)usersArray fromChat:(AIChat *)chat
1126 NSEnumerator *enumerator = [usersArray objectEnumerator];
1127 NSString *contactName;
1128 while(contactName = [enumerator nextObject]){
1129 [self removeUser:contactName fromChat:chat];
1133 /*********************/
1134 /* AIAccount_Privacy */
1135 /*********************/
1136 #pragma mark Privacy
1137 - (BOOL)addListObject:(AIListObject *)inObject toPrivacyList:(PRIVACY_TYPE)type
1139 if (type == PRIVACY_PERMIT)
1140 return (gaim_privacy_permit_add(account,[[inObject UID] UTF8String],FALSE));
1142 return (gaim_privacy_deny_add(account,[[inObject UID] UTF8String],FALSE));
1145 - (BOOL)removeListObject:(AIListObject *)inObject fromPrivacyList:(PRIVACY_TYPE)type
1147 if (type == PRIVACY_PERMIT)
1148 return (gaim_privacy_permit_remove(account,[[inObject UID] UTF8String],FALSE));
1150 return (gaim_privacy_deny_remove(account,[[inObject UID] UTF8String],FALSE));
1153 - (NSArray *)listObjectsOnPrivacyList:(PRIVACY_TYPE)type
1155 return (type == PRIVACY_PERMIT ? permittedContactsArray : deniedContactsArray);
1158 - (NSArray *)listObjectIDsOnPrivacyList:(PRIVACY_TYPE)type
1160 NSArray *listObjectArray = [self listObjectsOnPrivacyList:type];
1161 NSMutableArray *idArray = [[NSMutableArray alloc] initWithCapacity:[listObjectArray count]];
1162 NSEnumerator *enumerator = [listObjectArray objectEnumerator];
1163 AIListObject *object = nil;
1165 while(object = [enumerator nextObject]){
1166 [idArray addObject:[object UID]];
1169 return [idArray autorelease];
1172 - (oneway void)privacyPermitListAdded:(NSString *)sourceUID
1174 [self accountPrivacyList:PRIVACY_PERMIT added:sourceUID];
1177 - (oneway void)privacyDenyListAdded:(NSString *)sourceUID
1179 [self accountPrivacyList:PRIVACY_DENY added:sourceUID];
1182 - (void)accountPrivacyList:(PRIVACY_TYPE)type added:(NSString *)sourceUID
1184 //Can't really trust sourceUID to not be @"" or something silly like that
1185 if ([sourceUID length]){
1187 AIListContact *contact = [self contactWithUID:sourceUID];
1189 [(type == PRIVACY_PERMIT ? permittedContactsArray : deniedContactsArray) addObject:contact];
1193 - (oneway void)privacyPermitListRemoved:(NSString *)sourceUID
1195 [self accountPrivacyList:PRIVACY_PERMIT removed:sourceUID];
1198 - (oneway void)privacyDenyListRemoved:(NSString *)sourceUID
1200 [self accountPrivacyList:PRIVACY_DENY removed:sourceUID];
1203 - (void)accountPrivacyList:(PRIVACY_TYPE)type removed:(NSString *)sourceUID
1205 //Can't really trust sourceUID to not be @"" or something silly like that
1206 if ([sourceUID length]){
1207 if (!namesAreCaseSensitive){
1208 sourceUID = [sourceUID compactedString];
1211 //Get our contact, which must already exist for us to care about its removal
1212 AIListContact *contact = [[adium contactController] existingContactWithService:service
1217 [(type == PRIVACY_PERMIT ? permittedContactsArray : deniedContactsArray) removeObject:contact];
1222 - (void)setPrivacyOptions:(PRIVACY_OPTION)option
1224 if (account && gaim_account_get_connection(account)) {
1225 GaimPrivacyType privacyType;
1228 case PRIVACY_ALLOW_ALL:
1230 privacyType = GAIM_PRIVACY_ALLOW_ALL;
1232 case PRIVACY_DENY_ALL:
1233 privacyType = GAIM_PRIVACY_DENY_ALL;
1235 case PRIVACY_ALLOW_USERS:
1236 privacyType = GAIM_PRIVACY_ALLOW_USERS;
1238 case PRIVACY_DENY_USERS:
1239 privacyType = GAIM_PRIVACY_DENY_USERS;
1241 case PRIVACY_ALLOW_CONTACTLIST:
1242 privacyType = GAIM_PRIVACY_ALLOW_BUDDYLIST;
1246 account->perm_deny = privacyType;
1247 serv_set_permit_deny(gaim_account_get_connection(account));
1249 AILog(@"Couldn't set privacy options for %@ (%x %x)",self,account,gaim_account_get_connection(account));
1253 - (PRIVACY_OPTION)privacyOptions
1255 PRIVACY_OPTION privacyOption = -1;
1258 GaimPrivacyType privacyType = account->perm_deny;
1260 switch(privacyType){
1261 case GAIM_PRIVACY_ALLOW_ALL:
1263 privacyOption = PRIVACY_ALLOW_ALL;
1265 case GAIM_PRIVACY_DENY_ALL:
1266 privacyOption = PRIVACY_DENY_ALL;
1268 case GAIM_PRIVACY_ALLOW_USERS:
1269 privacyOption = PRIVACY_ALLOW_USERS;
1271 case GAIM_PRIVACY_DENY_USERS:
1272 privacyOption = PRIVACY_DENY_USERS;
1274 case GAIM_PRIVACY_ALLOW_BUDDYLIST:
1275 privacyOption = PRIVACY_ALLOW_CONTACTLIST;
1280 return(privacyOption);
1283 /*****************************************************/
1284 /* File transfer / AIAccount_Files inherited methods */
1285 /*****************************************************/
1286 #pragma mark File Transfer
1288 //Create a protocol-specific xfer object, set it up as requested, and begin sending
1289 - (void)_beginSendOfFileTransfer:(ESFileTransfer *)fileTransfer
1291 GaimXfer *xfer = [self newOutgoingXferForFileTransfer:fileTransfer];
1294 //Associate the fileTransfer and the xfer with each other
1295 [fileTransfer setAccountData:[NSValue valueWithPointer:xfer]];
1296 xfer->ui_data = [fileTransfer retain];
1299 gaim_xfer_set_local_filename(xfer, [[fileTransfer localFilename] UTF8String]);
1300 gaim_xfer_set_filename(xfer, [[[fileTransfer localFilename] lastPathComponent] UTF8String]);
1303 Request that the transfer begins.
1304 We will be asked to accept it via:
1305 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1308 [gaimThread xferRequest:xfer];
1311 //By default, protocols can not create GaimXfer objects
1312 - (GaimXfer *)newOutgoingXferForFileTransfer:(ESFileTransfer *)fileTransfer
1318 * @brief The account requested that we received a file.
1320 * Set up the ESFileTransfer and query the fileTransferController for a save location.
1322 * @result The window controller for the window prompting the user to take action, or nil if no prompt was shown
1324 - (NSWindowController *)requestReceiveOfFileTransfer:(ESFileTransfer *)fileTransfer
1326 GaimDebug(@"File transfer request received: %@",fileTransfer);
1327 return [[adium fileTransferController] receiveRequestForFileTransfer:fileTransfer];
1330 //Create an ESFileTransfer object from an xfer
1331 - (ESFileTransfer *)newFileTransferObjectWith:(NSString *)destinationUID
1332 size:(unsigned long long)inSize
1333 remoteFilename:(NSString *)remoteFilename
1335 return([self mainPerformSelector:@selector(_mainThreadNewFileTransferObjectWith:size:remoteFilename:)
1336 withObject:destinationUID
1337 withObject:[NSNumber numberWithUnsignedLongLong:inSize]
1338 withObject:remoteFilename
1341 - (ESFileTransfer *)_mainThreadNewFileTransferObjectWith:(NSString *)destinationUID
1342 size:(NSNumber *)inSize
1343 remoteFilename:remoteFilename
1345 AIListContact *contact = [self contactWithUID:destinationUID];
1346 ESFileTransfer *fileTransfer;
1348 fileTransfer = [[adium fileTransferController] newFileTransferWithContact:contact
1350 [fileTransfer setSize:[inSize unsignedLongLongValue]];
1351 [fileTransfer setRemoteFilename:remoteFilename];
1353 return(fileTransfer);
1356 //Update an ESFileTransfer object progress
1357 - (oneway void)updateProgressForFileTransfer:(ESFileTransfer *)fileTransfer percent:(NSNumber *)percent bytesSent:(NSNumber *)bytesSent
1359 float percentDone = [percent floatValue];
1360 [fileTransfer setPercentDone:percentDone bytesSent:[bytesSent unsignedLongValue]];
1363 //The local side canceled the transfer. We probably already have this status set, but set it just in case.
1364 - (oneway void)fileTransferCanceledLocally:(ESFileTransfer *)fileTransfer
1366 [fileTransfer setStatus:Canceled_Local_FileTransfer];
1369 //The remote side canceled the transfer, the fool. Update our status.
1370 - (oneway void)fileTransferCanceledRemotely:(ESFileTransfer *)fileTransfer
1372 [fileTransfer setStatus:Canceled_Remote_FileTransfer];
1375 - (oneway void)destroyFileTransfer:(ESFileTransfer *)fileTransfer
1377 GaimDebug(@"Destroy file transfer %@",fileTransfer);
1378 [fileTransfer release];
1381 //Accept a send or receive ESFileTransfer object, beginning the transfer.
1382 //Subsequently inform the fileTransferController that the fun has begun.
1383 - (void)acceptFileTransferRequest:(ESFileTransfer *)fileTransfer
1385 GaimDebug(@"Accepted file transfer %@",fileTransfer);
1388 GaimXferType xferType;
1390 xfer = [[fileTransfer accountData] pointerValue];
1392 xferType = gaim_xfer_get_type(xfer);
1393 if ( xferType == GAIM_XFER_SEND ) {
1394 [fileTransfer setType:Outgoing_FileTransfer];
1395 } else if ( xferType == GAIM_XFER_RECEIVE ) {
1396 [fileTransfer setType:Incoming_FileTransfer];
1397 [fileTransfer setSize:(xfer->size)];
1400 //accept the request
1401 [gaimThread xferRequestAccepted:xfer withFileName:[fileTransfer localFilename]];
1403 //set the size - must be done after request is accepted?
1406 [fileTransfer setStatus:Accepted_FileTransfer];
1409 //User refused a receive request. Tell gaim; we don't release the ESFileTransfer object
1410 //since that will happen when the xfer is destroyed. This will end up calling back on
1411 //- (oneway void)fileTransferCanceledLocally:(ESFileTransfer *)fileTransfer
1412 - (void)rejectFileReceiveRequest:(ESFileTransfer *)fileTransfer
1414 GaimXfer *xfer = [[fileTransfer accountData] pointerValue];
1416 [gaimThread xferRequestRejected:xfer];
1420 //Cancel a file transfer in progress. Tell gaim; we don't release the ESFileTransfer object
1421 //since that will happen when the xfer is destroyed. This will end up calling back on
1422 //- (oneway void)fileTransferCanceledLocally:(ESFileTransfer *)fileTransfer
1423 - (void)cancelFileTransfer:(ESFileTransfer *)fileTransfer
1425 GaimXfer *xfer = [[fileTransfer accountData] pointerValue];
1427 [gaimThread xferCancel:xfer];
1431 //Account Connectivity -------------------------------------------------------------------------------------------------
1432 #pragma mark Connect
1433 //Connect this account (Our password should be in the instance variable 'password' all ready for us)
1437 //create a gaim account if one does not already exist
1438 [self createNewGaimAccount];
1439 GaimDebug(@"created GaimAccount 0x%x with UID %@, protocolPlugin %s", account, [self UID], [self protocolPlugin]);
1443 [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Connecting" notify:NotifyNow];
1445 //Make sure our settings are correct
1446 [self configureGaimAccountNotifyingTarget:self selector:@selector(continueConnectWithConfiguredGaimAccount)];
1449 - (void)continueConnectWithConfiguredGaimAccount
1451 //Configure libgaim's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1452 [self configureAccountProxyNotifyingTarget:self selector:@selector(continueConnectWithConfiguredProxy)];
1455 - (void)continueConnectWithConfiguredProxy
1457 //Set password and connect
1458 gaim_account_set_password(account, [password UTF8String]);
1460 GaimDebug(@"Adium: Connect: %@ initiating connection.",[self UID]);
1462 [gaimThread connectAccount:self];
1464 GaimDebug(@"Adium: Connect: %@ done initiating connection %x.",[self UID], account->gc);
1468 //Make sure our settings are correct; notify target/selector when we're finished
1469 - (void)configureGaimAccountNotifyingTarget:(id)target selector:(SEL)selector
1471 NSInvocation *contextInvocation;
1473 //Perform the synchronous configuration activities (subclasses may want to take action in this function)
1474 [self configureGaimAccount];
1476 contextInvocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1478 [contextInvocation setTarget:target];
1479 [contextInvocation setSelector:selector];
1480 [contextInvocation retainArguments];
1482 //Set the text profile BEFORE beginning the connect process, to avoid problems with setting it while the
1483 //connect occurs. Once that's done, contextInvocation will be invoked, continuing the configureGaimAccount process.
1484 [self autoRefreshingOutgoingContentForStatusKey:@"TextProfile"
1485 selector:@selector(setAccountProfileTo:configureGaimAccountContext:)
1486 context:contextInvocation];
1489 //Synchronous gaim account configuration activites, always performed after an account is created.
1490 //This is a definite subclassing point so prpls can apply their own account settings.
1491 - (void)configureGaimAccount
1497 hostName = [self host];
1498 if (hostName && [hostName length]){
1499 gaim_account_set_string(account, "server", [hostName UTF8String]);
1503 portNumber = [self port];
1505 gaim_account_set_int(account, "port", portNumber);
1509 XXX: This is a hack for 0.8. Since we don't have a full privacy UI yet, we automatically set our privacy setting to
1510 the best one to use.
1512 [gaimThread setDefaultPermitDenyForAccount:self];
1515 gaim_account_set_check_mail(account, [[self shouldCheckMail] boolValue]);
1517 //Update a few status keys before we begin connecting. Libgaim will send these automatically
1518 [self updateStatusForKey:KEY_USER_ICON];
1521 //Configure libgaim's proxy settings using the current system values
1522 - (void)configureAccountProxyNotifyingTarget:(id)target selector:(SEL)selector
1524 GaimProxyInfo *proxy_info;
1525 GaimProxyType gaimAccountProxyType;
1527 NSNumber *proxyPref = [self preferenceForKey:KEY_ACCOUNT_PROXY_TYPE group:GROUP_ACCOUNT_STATUS];
1528 BOOL proxyEnabled = [[self preferenceForKey:KEY_ACCOUNT_PROXY_ENABLED group:GROUP_ACCOUNT_STATUS] boolValue];
1530 NSString *host = nil;
1531 NSString *proxyUserName = nil;
1532 NSString *proxyPassword = nil;
1533 AdiumProxyType proxyType;
1535 NSInvocation *invocation;
1537 //Configure the invocation we will use when we are done configuring
1538 invocation = [NSInvocation invocationWithMethodSignature:[target methodSignatureForSelector:selector]];
1539 [invocation setSelector:selector];
1540 [invocation setTarget:target];
1542 proxy_info = gaim_proxy_info_new();
1543 gaim_account_set_proxy_info(account, proxy_info);
1545 proxyType = (proxyPref ? [proxyPref intValue] : Adium_Proxy_Default_SOCKS5);
1549 gaim_proxy_info_set_type(proxy_info, GAIM_PROXY_NONE);
1550 GaimDebug(@"Adium: Connect: %@ Connecting with no proxy.",[self UID]);
1551 [invocation invoke];
1553 }else if ((proxyType == Adium_Proxy_Default_SOCKS5) ||
1554 (proxyType == Adium_Proxy_Default_HTTP) ||
1555 (proxyType == Adium_Proxy_Default_SOCKS4)) {
1556 //Load and use systemwide proxy settings
1557 NSDictionary *systemProxySettingsDictionary;
1558 ProxyType adiumProxyType = Proxy_None;
1560 if (proxyType == Adium_Proxy_Default_SOCKS5){
1561 gaimAccountProxyType = GAIM_PROXY_SOCKS5;
1562 adiumProxyType = Proxy_SOCKS5;
1564 }else if (proxyType == Adium_Proxy_Default_HTTP){
1565 gaimAccountProxyType = GAIM_PROXY_HTTP;
1566 adiumProxyType = Proxy_HTTP;
1568 }else if (proxyType == Adium_Proxy_Default_SOCKS4){
1569 gaimAccountProxyType = GAIM_PROXY_SOCKS4;
1570 adiumProxyType = Proxy_SOCKS4;
1573 GaimDebug(@"Loading proxy dictionary.");
1575 if((systemProxySettingsDictionary = [ESSystemNetworkDefaults systemProxySettingsDictionaryForType:adiumProxyType])) {
1577 GaimDebug(@"Retrieved %@",systemProxySettingsDictionary);
1579 host = [systemProxySettingsDictionary objectForKey:@"Host"];
1580 port = [[systemProxySettingsDictionary objectForKey:@"Port"] intValue];
1582 proxyUserName = [systemProxySettingsDictionary objectForKey:@"Username"];
1583 proxyPassword = [systemProxySettingsDictionary objectForKey:@"Password"];
1586 //Using system wide defaults, and no proxy of the specified type is set in the system preferences
1587 gaimAccountProxyType = GAIM_PROXY_NONE;
1590 gaim_proxy_info_set_type(proxy_info, gaimAccountProxyType);
1592 gaim_proxy_info_set_host(proxy_info, (char *)[host UTF8String]);
1593 gaim_proxy_info_set_port(proxy_info, port);
1595 if (proxyUserName && [proxyUserName length]){
1596 gaim_proxy_info_set_username(proxy_info, (char *)[proxyUserName UTF8String]);
1597 if (proxyPassword && [proxyPassword length]){
1598 gaim_proxy_info_set_password(proxy_info, (char *)[proxyPassword UTF8String]);
1602 GaimDebug(@"Systemwide proxy settings: %i %s:%i %s",proxy_info->type,proxy_info->host,proxy_info->port,proxy_info->username);
1604 [invocation invoke];
1607 host = [self preferenceForKey:KEY_ACCOUNT_PROXY_HOST group:GROUP_ACCOUNT_STATUS];
1608 port = [[self preferenceForKey:KEY_ACCOUNT_PROXY_PORT group:GROUP_ACCOUNT_STATUS] intValue];
1611 case Adium_Proxy_HTTP:
1612 gaimAccountProxyType = GAIM_PROXY_HTTP;
1614 case Adium_Proxy_SOCKS4:
1615 gaimAccountProxyType = GAIM_PROXY_SOCKS4;
1617 case Adium_Proxy_SOCKS5:
1618 gaimAccountProxyType = GAIM_PROXY_SOCKS5;
1621 gaimAccountProxyType = GAIM_PROXY_NONE;
1625 gaim_proxy_info_set_type(proxy_info, gaimAccountProxyType);
1626 gaim_proxy_info_set_host(proxy_info, (char *)[host UTF8String]);
1627 gaim_proxy_info_set_port(proxy_info, port);
1629 //If we need to authenticate, request the password and finish setting up the proxy in gotProxyServerPassword:context:
1630 proxyUserName = [self preferenceForKey:KEY_ACCOUNT_PROXY_USERNAME group:GROUP_ACCOUNT_STATUS];
1631 if (proxyUserName && [proxyUserName length]){
1632 gaim_proxy_info_set_username(proxy_info, (char *)[proxyUserName UTF8String]);
1634 [[adium accountController] passwordForProxyServer:host
1635 userName:proxyUserName
1636 notifyingTarget:self
1637 selector:@selector(gotProxyServerPassword:context:)
1638 context:invocation];
1641 GaimDebug(@"Adium proxy settings: %i %s:%i",proxy_info->type,proxy_info->host,proxy_info->port);
1642 [invocation invoke];
1647 //Retried the proxy password from the keychain
1648 - (void)gotProxyServerPassword:(NSString *)inPassword context:(NSInvocation *)invocation
1650 GaimProxyInfo *proxy_info = gaim_account_get_proxy_info(account);
1653 gaim_proxy_info_set_password(proxy_info, (char *)[inPassword UTF8String]);
1655 GaimDebug(@"GotPassword: Proxy settings: %i %s:%i %s",proxy_info->type,proxy_info->host,proxy_info->port,proxy_info->username);
1657 [invocation invoke];
1660 gaim_proxy_info_set_username(proxy_info, NULL);
1662 //We are no longer connecting
1663 [self setStatusObject:nil forKey:@"Connecting" notify:NotifyNow];
1667 //Sublcasses should override to provide a string for each progress step
1668 - (NSString *)connectionStringForStep:(int)step { return nil; };
1670 //Our account has connected
1671 - (oneway void)accountConnectionConnected
1673 AILog(@"************ %@ CONNECTED ***********",[self UID]);
1678 [self silenceAllContactUpdatesForInterval:18.0];
1679 [[adium contactController] delayListObjectNotificationsUntilInactivity];
1681 //Reset reconnection attempts
1682 reconnectAttemptsRemaining = RECONNECTION_ATTEMPTS;
1684 //Clear any previous disconnection error
1685 [lastDisconnectionError release]; lastDisconnectionError = nil;
1688 - (oneway void)accountConnectionProgressStep:(NSNumber *)step percentDone:(NSNumber *)connectionProgressPrecent
1690 NSString *connectionProgressString = [self connectionStringForStep:[step intValue]];
1692 [self setStatusObject:connectionProgressString forKey:@"ConnectionProgressString" notify:NO];
1693 [self setStatusObject:connectionProgressPrecent forKey:@"ConnectionProgressPercent" notify:NO];
1696 [self notifyOfChangedStatusSilently:NO];
1698 AILog(@"************ %@ --step-- %i",[self UID],[step intValue]);
1701 - (void)createNewGaimAccount
1703 //Create a fresh version of the account
1704 account = gaim_account_new([[self formattedUID] UTF8String], [self protocolPlugin]);
1705 account->perm_deny = GAIM_PRIVACY_DENY_USERS;
1707 [self finishCreateNewGaimAccount];
1710 - (void)finishCreateNewGaimAccount
1713 gaimThread = [[SLGaimCocoaAdapter sharedInstance] retain];
1716 [gaimThread addAdiumAccount:self];
1719 #pragma mark Disconnect
1722 * @brief Disconnect this account
1726 if ([self online] || [self integerStatusObjectForKey:@"Connecting"]){
1727 //As per AIAccount's documentation, call super's implementation
1730 [self setStatusObject:nil forKey:@"Connecting" notify:NO];
1731 [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Disconnecting" notify:NotifyNow];
1732 [[adium contactController] delayListObjectNotificationsUntilInactivity];
1734 //Tell libgaim to disconnect
1735 [gaimThread disconnectAccount:self];
1740 * @brief Our account was unexpectedly disconnected with an error message
1742 - (oneway void)accountConnectionReportDisconnect:(NSString *)text
1744 //Retain the error message locally for use in -[CBGaimAccount accountConnectionDisconnected]
1745 if (lastDisconnectionError != text) {
1746 [lastDisconnectionError release];
1747 lastDisconnectionError = [text retain];
1750 //We are disconnecting
1751 [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Disconnecting" notify:NotifyNow];
1753 GaimDebug(@"%@ reported disconnecting: %@",[self UID],lastDisconnectionError);
1756 - (oneway void)accountConnectionNotice:(NSString *)connectionNotice
1758 [[adium interfaceController] handleErrorMessage:[NSString stringWithFormat:AILocalizedString(@"%@ (%@) : Connection Notice",nil),[self formattedUID],[service description]]
1759 withDescription:connectionNotice];
1764 * @brief Our account has disconnected
1766 * This is called after the accoutn disconnects for any reason
1768 - (oneway void)accountConnectionDisconnected
1770 BOOL connectionIsSuicidal = ((account && account->gc) ? account->gc->wants_to_die : NO);
1772 //We are now offline
1773 [self setStatusObject:nil forKey:@"Disconnecting" notify:NO];
1774 [self setStatusObject:nil forKey:@"Connecting" notify:NO];
1775 [self setStatusObject:nil forKey:@"Online" notify:NO];
1777 //Clear status objects which don't make sense for a disconnected account
1778 [self setStatusObject:nil forKey:@"TextProfile" notify:NO];
1781 [self notifyOfChangedStatusSilently:NO];
1783 //If we were disconnected unexpectedly, attempt a reconnect. Give subclasses a chance to handle the disconnection error.
1784 //connectionIsSuicidal == TRUE when Gaim thinks we shouldn't attempt a reconnect.
1785 if([[self preferenceForKey:@"Online" group:GROUP_ACCOUNT_STATUS] boolValue]/* && lastDisconnectionError*/){
1786 if (reconnectAttemptsRemaining &&
1787 [self shouldAttemptReconnectAfterDisconnectionError:&lastDisconnectionError] && !(connectionIsSuicidal)) {
1789 [self autoReconnectAfterDelay:AUTO_RECONNECT_DELAY];
1790 reconnectAttemptsRemaining--;
1792 if (lastDisconnectionError){
1793 //Display then clear the last disconnection error
1794 [self displayError:lastDisconnectionError];
1795 [lastDisconnectionError release]; lastDisconnectionError = nil;
1798 //Reset reconnection attempts
1799 reconnectAttemptsRemaining = RECONNECTION_ATTEMPTS;
1801 //Clear our desire to be online.
1802 [self setPreference:nil
1804 group:GROUP_ACCOUNT_STATUS];
1808 //Report that we disconnected
1809 [self didDisconnect];
1812 //By default, always attempt to reconnect. Subclasses may override this to manage reconnect behavior.
1813 - (BOOL)shouldAttemptReconnectAfterDisconnectionError:(NSString **)disconnectionError
1818 #pragma mark Registering
1819 - (void)performRegisterWithPassword:(NSString *)inPassword
1821 //Save the new password
1822 if(password != inPassword){
1823 [password release]; password = [inPassword retain];
1827 //create a gaim account if one does not already exist
1828 [self createNewGaimAccount];
1829 GaimDebug(@"Registering: created GaimAccount 0x%x with UID %@, protocolPlugin %s", account, [self UID], [self protocolPlugin]);
1833 [self setStatusObject:[NSNumber numberWithBool:YES] forKey:@"Connecting" notify:NotifyNow];
1835 //Make sure our settings are correct
1836 [self configureGaimAccountNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredGaimAccount)];
1839 - (void)continueRegisterWithConfiguredGaimAccount
1841 //Configure libgaim's proxy settings; continueConnectWithConfiguredProxy will be called once we are ready
1842 [self configureAccountProxyNotifyingTarget:self selector:@selector(continueRegisterWithConfiguredProxy)];
1845 - (void)continueRegisterWithConfiguredProxy
1847 //Set password and connect
1848 gaim_account_set_password(account, [password UTF8String]);
1850 GaimDebug(@"Adium: Register: %@ initiating connection.",[self UID]);
1852 [gaimThread registerAccount:self];
1855 //Account Status ------------------------------------------------------------------------------------------------------
1856 #pragma mark Account Status
1857 //Status keys this account supports
1858 - (NSSet *)supportedPropertyKeys
1860 static NSMutableSet *supportedPropertyKeys = nil;
1862 if (!supportedPropertyKeys){
1863 supportedPropertyKeys = [[NSMutableSet alloc] initWithObjects:
1869 @"DefaultUserIconFilename",
1870 KEY_ACCOUNT_CHECK_MAIL,
1872 [supportedPropertyKeys unionSet:[super supportedPropertyKeys]];
1876 return supportedPropertyKeys;
1880 - (void)updateStatusForKey:(NSString *)key
1882 [super updateStatusForKey:key];
1884 //Now look at keys which only make sense if we have an account
1886 GaimDebug(@"%@: Updating status for key: %@",self, key);
1888 if([key isEqualToString:@"IdleSince"]){
1889 NSDate *idleSince = [self preferenceForKey:@"IdleSince" group:GROUP_ACCOUNT_STATUS];
1890 [self setAccountIdleSinceTo:idleSince];
1892 }else if([key isEqualToString:@"TextProfile"]){
1893 [self autoRefreshingOutgoingContentForStatusKey:key selector:@selector(setAccountProfileTo:)];
1895 }else if([key isEqualToString:KEY_USER_ICON]){
1896 NSData *data = [self preferenceForKey:KEY_USER_ICON group:GROUP_ACCOUNT_STATUS];
1898 [self setAccountUserImageData:data];
1900 }else if([key isEqualToString:KEY_ACCOUNT_CHECK_MAIL]){
1901 //Update the mail checking setting if the account is already made (if it isn't, we'll set it when it is made)
1903 [gaimThread setCheckMail:[self shouldCheckMail]
1911 * @brief Perform the setting of a status state
1913 * Sets the account to a passed status state. The account should set itself to best possible status given the return
1914 * values of statusState's accessors. The passed statusMessage has been filtered; it should be used rather than
1915 * [statusState statusMessage], which returns an unfiltered statusMessage.
1917 * @param statusState The state to enter
1918 * @param statusMessage The filtered status message to use.
1920 - (void)setStatusState:(AIStatus *)statusState usingStatusMessage:(NSAttributedString *)statusMessage
1923 char *gaimStatusType;
1924 NSString *encodedStatusMessage;
1926 //Get the gaim status type from this class or subclasses, which may also potentially modify or nullify our statusMessage
1927 gaimStatusType = [self gaimStatusTypeForStatus:statusState
1928 message:&statusMessage];
1930 //Encode the status message if we still have one
1931 encodedStatusMessage = (statusMessage ?
1932 [self encodedAttributedString:statusMessage
1933 forGaimStatusType:gaimStatusType] :
1936 [self setStatusState:statusState withGaimStatusType:gaimStatusType andMessage:encodedStatusMessage];
1941 * @brief Return the gaim status type to be used for a status
1943 * Active services provided nonlocalized status names. An AIStatus is passed to this method along with a pointer
1944 * to the status message. This method should handle any status whose statusNname this service set as well as any statusName
1945 * defined in AIStatusController.h (which will correspond to the services handled by Adium by default).
1946 * It should also handle a status name not specified in either of these places with a sane default, most likely by loooking at
1947 * [statusState statusType] for a general idea of the status's type.
1949 * @param statusState The status for which to find the gaim status equivalent
1950 * @param statusMessage A pointer to the statusMessage. Set *statusMessage to nil if it should not be used directly for this status.
1952 * @result The gaim status equivalent
1954 - (char *)gaimStatusTypeForStatus:(AIStatus *)statusState
1955 message:(NSAttributedString **)statusMessage
1957 AIStatusType statusType = [statusState statusType];
1958 char *gaimStatusType = NULL;
1960 /* CBGaimAccount just handles available and away in the most simple way possible;
1961 * we don't even care what the statusName is. */
1963 case AIAvailableStatusType:
1964 gaimStatusType = "Available";
1966 case AIAwayStatusType:
1967 case AIInvisibleStatusType: /* Invisible defaults to just being an away status */
1968 gaimStatusType = GAIM_AWAY_CUSTOM;
1969 //If we make it here, and we don't have a status message, generate one from the status controller's description.
1970 if((*statusMessage == nil) || ([*statusMessage length] == 0)){
1971 *statusMessage = [NSAttributedString stringWithString:[[adium statusController] descriptionForStateOfStatus:statusState]];
1974 case AIOfflineStatusType:
1975 //I'm really unsure how we actually get here with AIOfflineStatusType, but ensure this function doesn't return NULL
1976 gaimStatusType = "";
1980 return gaimStatusType;
1984 * @brief Perform the actual setting a state
1986 * This is called by setStatusState. It allows subclasses to perform any other behaviors, such as modifying a display
1987 * name, which are called for by the setting of the state; most of the processing has already been done, however, so
1988 * most subclasses will not need to implement this.
1990 * @param statusState The AIStatus which is being set
1991 * @param gaimStatusType The status type which will be passed to Gaim, or NULL if Gaim's status will not be set for this account
1992 * @param statusMessage A properly encoded message which will be associated with the status if possible.
1994 - (void)setStatusState:(AIStatus *)statusState withGaimStatusType:(const char *)gaimStatusType andMessage:(NSString *)statusMessage
1996 [gaimThread setGaimStatusType:gaimStatusType
1997 withMessage:statusMessage
2001 //Set our idle (Pass nil for no idle)
2002 - (void)setAccountIdleSinceTo:(NSDate *)idleSince
2004 [gaimThread setIdleSinceTo:idleSince onAccount:self];
2006 //We now should update our idle status object
2007 [self setStatusObject:([idleSince timeIntervalSinceNow] ? idleSince : nil)
2012 //Set the profile, then invoke the passed invocation to return control to the target/selector specified
2013 //by a configureGaimAccountNotifyingTarget:selector: call.
2014 - (void)setAccountProfileTo:(NSAttributedString *)profile configureGaimAccountContext:(NSInvocation *)inInvocation
2016 [self setAccountProfileTo:profile];
2018 [inInvocation invoke];
2021 //Set our profile immediately on the gaimThread
2022 - (void)setAccountProfileTo:(NSAttributedString *)profile
2024 if(!profile || ![[profile string] isEqualToString:[[self statusObjectForKey:@"TextProfile"] string]]){
2025 NSString *profileHTML = nil;
2027 //Convert the profile to HTML, and pass it to libgaim
2029 profileHTML = [self encodedAttributedString:profile forListObject:nil];
2032 [gaimThread setInfo:profileHTML onAccount:self];
2034 //We now have a profile
2035 [self setStatusObject:profile forKey:@"TextProfile" notify:NotifyNow];
2040 * @brief Set our user image
2042 * Pass nil for no image. This resizes and converts the image as needed for our protocol.
2043 * After setting it with gaim, it sets it within Adium; if this is not called, the image will
2044 * show up neither locally nor remotely.
2046 - (void)setAccountUserImageData:(NSData *)originalData
2048 NSImage *image = (originalData ? [[[NSImage alloc] initWithData:originalData] autorelease] : nil);
2051 NSSize imageSize = [image size];
2053 //Clear the existing icon first
2054 [gaimThread setBuddyIcon:nil onAccount:self];
2056 /* Now pass libgaim the new icon. Libgaim takes icons as a file, so we save our
2057 * image to one, and then pass libgaim the path. Check to be sure our image doesn't have an NSZeroSize size,
2058 * which would indicate currupt data */
2059 if (image && !NSEqualSizes(NSZeroSize, imageSize)) {
2060 GaimPluginProtocolInfo *prpl_info = GAIM_PLUGIN_PROTOCOL_INFO(gaim_find_prpl(account->protocol_id));
2061 GaimDebug(@"Original image of size %f %f",imageSize.width,imageSize.height);
2063 if (prpl_info && (prpl_info->icon_spec.format)) {
2064 NSString *buddyIconFilename = [self _userIconCachePath];
2065 NSData *buddyIconData = nil;
2066 BOOL smallEnough, prplScales;
2069 /* We need to scale it down if:
2070 * 1) The prpl needs to scale before it sends to the server or other buddies AND
2071 * 2) The image is larger than the maximum size allowed by the protocol
2072 * We ignore the minimum required size, as scaling up just leads to pixellated images.
2074 smallEnough = (prpl_info->icon_spec.max_width >= imageSize.width &&
2075 prpl_info->icon_spec.max_height >= imageSize.height);
2077 prplScales = (prpl_info->icon_spec.scale_rules & GAIM_ICON_SCALE_SEND) || (prpl_info->icon_spec.scale_rules & GAIM_ICON_SCALE_DISPLAY);
2079 if (prplScales && !smallEnough) {
2080 //Determine the scaled size. If it's too big, scale to the largest permissable size
2081 image = [image imageByScalingToSize:NSMakeSize(prpl_info->icon_spec.max_width,
2082 prpl_info->icon_spec.max_height)];
2084 /* Our original data is no longer valid, since we had to scale to a different size */
2086 GaimDebug(@"Scaled image to size %@",NSStringFromSize([image size]));
2089 if (!buddyIconData) {
2090 char **prpl_formats = g_strsplit(prpl_info->icon_spec.format,",",0);
2092 //Look for gif first if the image is animated
2093 NSImageRep *imageRep = [image bestRepresentationForDevice:nil] ;
2094 if ([imageRep isKindOfClass:[NSBitmapImageRep class]] &&
2095 [[(NSBitmapImageRep *)imageRep valueForProperty:NSImageFrameCount] intValue] > 1) {
2097 for (i = 0; prpl_formats[i]; i++) {
2098 if (strcmp(prpl_formats[i],"gif") == 0) {
2099 /* Try to use our original data. If we had to scale, originalData will have been set
2100 * to nil and we'll continue below to convert the image. */
2101 GaimDebug(@"l33t script kiddie animated GIF!!111");
2103 buddyIconData = originalData;
2110 if (!buddyIconData) {
2111 for (i = 0; prpl_formats[i]; i++) {
2112 if (strcmp(prpl_formats[i],"png") == 0) {
2113 buddyIconData = [image PNGRepresentation];
2117 } else if ((strcmp(prpl_formats[i],"jpeg") == 0) || (strcmp(prpl_formats[i],"jpg") == 0)) {
2118 /* OS X 10.4's JPEG representation does much better than 10.3's. Unfortunately, that also
2119 * means larger file sizes... which for our only JPEG-based protocol, AIM, means the buddy
2120 * icon doesn't get sent. AIM max is 8 kilobytes; 10.4 produces 12 kb images. 0.90 is
2121 * large indistinguishable from 1.0 anyways.
2123 float compressionFactor = ([NSApp isOnTigerOrBetter] ?
2127 buddyIconData = [image JPEGRepresentationWithCompressionFactor:compressionFactor];
2131 } else if ((strcmp(prpl_formats[i],"tiff") == 0) || (strcmp(prpl_formats[i],"tif") == 0)) {
2132 buddyIconData = [image TIFFRepresentation];
2136 } else if (strcmp(prpl_formats[i],"gif") == 0) {
2137 buddyIconData = [image GIFRepresentation];
2141 } else if (strcmp(prpl_formats[i],"bmp") == 0) {
2142 buddyIconData = [image BMPRepresentation];
2151 g_strfreev(prpl_formats);
2154 if ([buddyIconData writeToFile:buddyIconFilename atomically:YES]) {
2155 [gaimThread setBuddyIcon:buddyIconFilename onAccount:self];
2158 GaimDebug(@"Error writing file %@",buddyIconFilename);
2164 //We now have an icon
2165 [self setStatusObject:image forKey:KEY_USER_ICON notify:NotifyNow];
2168 #pragma mark Group Chat
2169 - (BOOL)inviteContact:(AIListContact *)inContact toChat:(AIChat *)inChat withMessage:(NSString *)inviteMessage
2171 [gaimThread inviteContact:inContact toChat:inChat withMessage:inviteMessage];
2176 #pragma mark Buddy Menu Items
2177 //Returns an array of menuItems specific for this contact based on its account and potentially status
2178 - (NSArray *)menuItemsForContact:(AIListContact *)inContact
2180 NSMutableArray *menuItemArray = nil;
2181 if (account && gaim_account_is_connected(account)){
2182 GaimPluginProtocolInfo *prpl_info = GAIM_PLUGIN_PROTOCOL_INFO(account->gc->prpl);
2187 //Find the GaimBuddy
2188 buddy = gaim_find_buddy(account, gaim_normalize(account, [[inContact UID] UTF8String]));
2190 if(prpl_info && prpl_info->blist_node_menu && buddy){
2191 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:[self service]
2192 type:AIServiceIconSmall
2193 direction:AIIconNormal];
2195 //Add a NSMenuItem for each node action specified by the prpl
2196 for(l = ll = prpl_info->blist_node_menu((GaimBlistNode *)buddy); l; l = l->next) {
2197 GaimBlistNodeAction *act = (GaimBlistNodeAction *) l->data;
2199 NSMenuItem *menuItem;
2202 //If titleForContactMenuLabel:forContact: returns nil, we don't add the menuItem
2205 (title = [self titleForContactMenuLabel:act->label
2206 forContact:inContact])) {
2207 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2209 action:@selector(performContactMenuAction:)
2210 keyEquivalent:@""] autorelease];
2211 [menuItem setImage:serviceIcon];
2212 dict = [NSDictionary dictionaryWithObjectsAndKeys:
2213 [NSValue valueWithPointer:act],@"GaimBlistNodeAction",
2214 [NSValue valueWithPointer:buddy],@"GaimBuddy",
2217 if(!menuItemArray) menuItemArray = [NSMutableArray array];
2219 [menuItem setRepresentedObject:dict];
2220 [menuItemArray addObject:menuItem];
2227 return(menuItemArray);
2230 //Action of a dynamically-generated contact menu item
2231 - (void)performContactMenuAction:(NSMenuItem *)sender
2233 NSDictionary *dict = [sender representedObject];
2235 [gaimThread performContactMenuActionFromDict:dict];
2238 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2239 - (NSString *)titleForContactMenuLabel:(const char *)label forContact:(AIListContact *)inContact
2241 return([NSString stringWithUTF8String:label]);
2245 * @brief Menu items for the account's actions
2247 * Returns an array of menu items for account-specific actions. This is the best place to add protocol-specific
2248 * actions that aren't otherwise supported by Adium. It will only be queried if the account is online.
2249 * @return NSArray of NSMenuItem instances for this account
2251 - (NSArray *)accountActionMenuItems
2253 NSMutableArray *menuItemArray = nil;
2255 if (account && gaim_account_is_connected(account)){
2256 GaimPlugin *plugin = account->gc->prpl;
2258 if(GAIM_PLUGIN_HAS_ACTIONS(plugin)){
2261 //Avoid adding separators between nonexistant items (i.e. items which Gaim shows but we don't)
2262 BOOL addedAnAction = NO;
2263 NSImage *serviceIcon = [AIServiceIcons serviceIconForService:[self service]
2264 type:AIServiceIconSmall
2265 direction:AIIconNormal];
2266 for (l = ll = GAIM_PLUGIN_ACTIONS(plugin, account->gc); l; l = l->next) {
2269 GaimPluginAction *action;
2271 NSMenuItem *menuItem;
2274 action = (GaimPluginAction *) l->data;
2276 //If titleForAccountActionMenuLabel: returns nil, we don't add the menuItem
2279 (title = [self titleForAccountActionMenuLabel:action->label])) {
2281 action->plugin = plugin;
2282 action->context = account->gc;
2284 menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
2286 action:@selector(performAccountMenuAction:)
2287 keyEquivalent:@""] autorelease];
2288 [menuItem setImage:serviceIcon];
2289 dict = [NSDictionary dictionaryWithObject:[NSValue valueWithPointer:action]
2290 forKey:@"GaimPluginAction"];
2292 [menuItem setRepresentedObject:dict];
2294 if(!menuItemArray) menuItemArray = [NSMutableArray array];
2296 [menuItemArray addObject:menuItem];
2297 addedAnAction = YES;
2304 [menuItemArray addObject:[NSMenuItem separatorItem]];
2314 return menuItemArray;
2317 //Action of a dynamically-generated contact menu item
2318 - (void)performAccountMenuAction:(NSMenuItem *)sender
2320 NSDictionary *dict = [sender representedObject];
2322 [gaimThread performAccountMenuActionFromDict:dict];
2325 //Subclasses may override to provide a localized label and/or prevent a specified label from being shown
2326 - (NSString *)titleForAccountActionMenuLabel:(const char *)label
2328 if((strcmp(label, "Change Password...") == 0) || (strcmp(label, "Change Password") == 0)){
2329 /* XXX This depends upon an implementation of adiumGaimRequestFields in adiumGaimRequest.m.
2330 * Enable once that is done. */
2334 return([NSString stringWithUTF8String:label]);
2337 /* Secure messaging */
2338 #pragma mark Secure Messaging
2339 - (void)requestSecureMessaging:(BOOL)inSecureMessaging
2340 inChat:(AIChat *)inChat
2342 [gaimThread requestSecureMessaging:inSecureMessaging
2346 - (void)promptToVerifyEncryptionIdentityInChat:(AIChat *)inChat
2348 [gaimThread promptToVerifyEncryptionIdentityInChat:inChat];
2351 - (BOOL)allowSecureMessagingTogglingForChat:(AIChat *)inChat
2353 //Allow secure messaging via OTR for one-on-one chats
2354 return([inChat name] == nil);
2357 - (NSString *)aboutEncryption
2359 return([NSString stringWithFormat:
2360 AILocalizedString(@"Adium provides encryption, authentication, deniability, and perfect forward secrecy over %@ via Off-the-Record Messaging (OTR). If your contact is not using an OTR-compatible messaging system, your contact will be sent a link to the OTR web site when you attempt to connect. For more information on OTR, visit http://www.cypherpunks.ca/otr/.",nil),
2361 [[self service] shortDescription]]);
2364 /********************************/
2365 /* AIAccount subclassed methods */
2366 /********************************/
2367 #pragma mark AIAccount Subclassed Methods
2370 NSDictionary *defaults = [NSDictionary dictionaryNamed:[NSString stringWithFormat:@"GaimDefaults%@",[[self service] serviceID]]
2371 forClass:[self class]];
2374 [[adium preferenceController] registerDefaults:defaults
2375 forGroup:GROUP_ACCOUNT_STATUS
2378 GaimDebug(@"Failed to load defaults for %@",[NSString stringWithFormat:@"GaimDefaults%@",[[self service] serviceID]]);
2382 reconnectAttemptsRemaining = RECONNECTION_ATTEMPTS;
2383 lastDisconnectionError = nil;
2385 permittedContactsArray = [[NSMutableArray alloc] init];
2386 deniedContactsArray = [[NSMutableArray alloc] init];
2388 //We will create a gaimAccount the first time we attempt to connect
2391 //Observe preferences changes
2392 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_ALIASES];
2396 * @brief The account's UID changed
2398 - (void)didChangeUID
2400 //Only need to take action if we have a created GaimAccount already
2401 if(account != NULL){
2402 //Remove our current account
2403 [gaimThread removeAdiumAccount:self];
2405 //Clear the reference to the GaimAccount... it'll be created when needed
2412 [[adium preferenceController] unregisterPreferenceObserver:self];
2414 [lastDisconnectionError release]; lastDisconnectionError = nil;
2416 [permittedContactsArray release];
2417 [deniedContactsArray release];
2422 - (NSString *)unknownGroupName {
2423 return (@"Unknown");
2426 - (NSDictionary *)defaultProperties { return([NSDictionary dictionary]); }
2428 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject
2430 return [inAttributedString string]; //Default behavior is plain text
2433 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forListObject:(AIListObject *)inListObject contentMessage:(AIContentMessage *)contentMessage
2435 return [self encodedAttributedString:inAttributedString forListObject:inListObject];
2438 - (NSString *)encodedAttributedString:(NSAttributedString *)inAttributedString forGaimStatusType:(const char *)gaimStatusType
2440 return [self encodedAttributedString:inAttributedString forListObject:nil];
2443 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
2444 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
2446 [super preferencesChangedForGroup:group key:key object:object preferenceDict:prefDict firstTime:firstTime];
2448 if([group isEqualToString:PREF_GROUP_ALIASES]){
2449 //If the notification object is a listContact belonging to this account, update the serverside information
2450 if ((account != nil) &&
2451 ([self shouldSetAliasesServerside]) &&
2452 ([key isEqualToString:@"Alias"])){
2454 NSString *alias = [object preferenceForKey:@"Alias"
2455 group:PREF_GROUP_ALIASES
2456 ignoreInheritedValues:YES];
2458 if([object isKindOfClass:[AIMetaContact class]]){
2459 NSEnumerator *enumerator = [[(AIMetaContact *)object containedObjects] objectEnumerator];
2460 AIListContact *containedListContact;
2461 while(containedListContact = [enumerator nextObject]){
2462 if([containedListContact account] == self){
2463 [gaimThread setAlias:alias forUID:[containedListContact UID] onAccount:self];
2467 }else if([object isKindOfClass:[AIListContact class]]){
2468 if([(AIListContact *)object account] == self){
2469 [gaimThread setAlias:alias forUID:[object UID] onAccount:self];
2477 /***************************/
2478 /* Account private methods */
2479 /***************************/
2480 #pragma mark Private
2481 - (void)setTypingFlagOfChat:(AIChat *)chat to:(NSNumber *)typingStateNumber
2483 AITypingState currentTypingState = [chat integerStatusObjectForKey:KEY_TYPING];
2484 AITypingState newTypingState = [typingStateNumber intValue];
2486 if(currentTypingState != newTypingState){
2487 [chat setStatusObject:(newTypingState ? typingStateNumber : nil)
2493 - (void)displayError:(NSString *)errorDesc
2495 [[adium interfaceController] handleErrorMessage:[NSString stringWithFormat:@"%@ (%@) : Gaim error",[self UID],[[self service] shortDescription]]
2496 withDescription:errorDesc];
2499 - (NSString *)_userIconCachePath
2501 NSString *userIconCacheFilename = [NSString stringWithFormat:@"TEMP-UserIcon_%@_%@", [self internalObjectID], [NSString randomStringOfLength:4]];
2502 return([[adium cachesPath] stringByAppendingPathComponent:userIconCacheFilename]);
2505 - (AIListContact *)contactWithUID:(NSString *)inUID
2507 return [super contactWithUID:inUID];
2510 - (AIListContact *)mainThreadContactWithUID:(NSString *)inUID
2512 AIListContact *contact;
2514 contact = [self mainPerformSelector:@selector(contactWithUID:)
2521 - (NSNumber *)shouldCheckMail
2523 return([self preferenceForKey:KEY_ACCOUNT_CHECK_MAIL group:GROUP_ACCOUNT_STATUS]);
2526 - (BOOL)displayConversationClosed
2531 - (BOOL)displayConversationTimedOut
2536 - (BOOL)shouldSetAliasesServerside
2541 - (NSString *)internalObjectID
2543 return([super internalObjectID]);